﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace C4F.DevKit.Calendar.Parser
{
    /// <summary>
    /// A class used to create and add to iCal files.
    /// </summary>
    public class Writer
    {
        #region Private Members

        private StreamWriter _streamWriter;

        #endregion

        #region Public Properties

        /// <summary>
        /// The path of the file to write the calendar to.
        /// </summary>
        public string FilePath
        {
            set
            {
                _streamWriter = new StreamWriter(new FileStream(value, FileMode.OpenOrCreate));
            }

            get
            {
                return ((FileStream)_streamWriter.BaseStream).Name;
            }
        }

        #endregion

        #region Public Constructors

        /// <summary>
        /// Creates a new calendar writer.
        /// </summary>
        /// <param name="filepath">The path for the iCal file to write to.</param>
        public Writer(string filepath)
        {
            FilePath = filepath;
        }

        /// <summary>
        /// Creates a new calendar writer.
        /// </summary>
        /// <param name="fileStream">The FileStream to write the iCal file to.</param>
        public Writer(FileStream fileStream)
        {
            if (!fileStream.CanRead)
            {
                throw new ArgumentException("Cannot read from file stream");
            }

            if (!fileStream.CanWrite)
            {
                throw new ArgumentException("Cannot write to file stream");
            }

            _streamWriter = new StreamWriter(fileStream);
        }

        /// <summary>
        /// Creates a new calendar writer.
        /// </summary>
        /// <param name="streamWriter">The StreamWriter that is attached to the file to write the iCal file to.</param>
        public Writer(StreamWriter streamWriter)
        {
            if (!streamWriter.BaseStream.CanRead)
            {
                throw new ArgumentException("Cannot read from base stream of StreamWriter");
            }

            _streamWriter = streamWriter;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new iCal file.
        /// </summary>
        /// <param name="appts">The appointments to write to the calendar.</param>
        public void CreateCalendar(IEnumerable<Appointment> appts)
        {
            Validate(appts);
            string filepath = FilePath;
            _streamWriter.Close();
            File.Delete(filepath);
            FilePath = filepath;
            _streamWriter.WriteLine(CalendarConstants.BEGIN_CALENDAR);
            _streamWriter.WriteLine(CalendarConstants.CALENDAR_METADATA);

            foreach (Appointment appt in appts)
            {
                Write(appt);
            }

            WriteCalendarEnd();
        }

        /// <summary>
        /// Adds a new appointment to an existing calendar.
        /// </summary>
        /// <param name="appt">The appointment to add.</param>
        public void AddToCalendar(Appointment appt)
        {
            List<Appointment> appts = new List<Appointment>();
            appts.Add(appt);
            AddToCalendar(appts);
        }

        /// <summary>
        /// Adds new appointments to an exisiting calendar.
        /// </summary>
        /// <param name="appts">The appointments to add.</param>
        public void AddToCalendar(IEnumerable<Appointment> appts)
        {
            Validate(appts);
            _streamWriter.BaseStream.Seek(0, SeekOrigin.Begin);
            StreamReader streamReader = new StreamReader(_streamWriter.BaseStream);
            string currentLine = streamReader.ReadLine();

            while (!currentLine.StartsWith(CalendarConstants.END_CALENDAR))
            {
                currentLine = streamReader.ReadLine();
            }

            _streamWriter.BaseStream.Seek(-1 * (currentLine.Length + _streamWriter.NewLine.Length), SeekOrigin.Current);

            foreach (Appointment appt in appts)
            {
                Write(appt);
            }

            WriteCalendarEnd();
        }

        /// <summary>
        /// Updates the information in an existing appointment.
        /// </summary>
        /// <param name="appt">The new version of the appointment to put in the calendar. This appointment
        /// will replace the appointment in the Calendar with the same UID.</param>
        public void EditAppointment(Appointment appt)
        {
            Validate(appt);
            IEnumerable<Appointment> appts = Reader.ReadCalendar((FileStream)_streamWriter.BaseStream);
            List<Appointment> apptsToWrite = new List<Appointment>();
            foreach (Appointment currAppt in appts)
            {
                if (!currAppt.UID.Equals(appt.UID))
                {
                    apptsToWrite.Add(currAppt);
                }
            }

            apptsToWrite.Add(appt);
            string filePath = ((FileStream)_streamWriter.BaseStream).Name;
            File.Delete(filePath);
            _streamWriter = new StreamWriter(filePath);
            CreateCalendar(apptsToWrite);
        }

        /// <summary>
        /// Writes the iCal data out to disk and closes the file.
        /// </summary>
        public void Close()
        {
            _streamWriter.Close();
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        /// Creates a new iCal file.
        /// </summary>
        /// <param name="appts">The appointments to write to the calendar.</param>
        /// <param name="filePath">The path of the iCal file to write to.</param>
        public static void CreateCalendar(IEnumerable<Appointment> appts, string filePath)
        {
            Writer writer = new Writer(filePath);
            writer.CreateCalendar(appts);
            writer.Close();
        }

        /// <summary>
        /// Adds new appointments to an exisiting calendar.
        /// </summary>
        /// <param name="appts">The appointments to add.</param>
        /// <param name="filePath">The path of the iCal file to write to.</param>
        public static void AddToCalendar(IEnumerable<Appointment> appts, string filePath)
        {
            Writer writer= new Writer(filePath);
            writer.AddToCalendar(appts);
            writer.Close();
        }

        /// <summary>
        /// Adds a new appointment to an existing calendar.
        /// </summary>
        /// <param name="appt">The appointment to add.</param>
        /// <param name="filePath">The path of the iCal file to write to.</param>
        public static void AddToCalendar(Appointment appt, string filePath)
        {
            Writer writer = new Writer(filePath);
            writer.AddToCalendar(appt);
            writer.Close();
        }

        /// <summary>
        /// Updates the information in an existing appointment.
        /// </summary>
        /// <param name="appt">The new version of the appointment to put in the calendar. This appointment
        /// will replace the appointment in the Calendar with the same UID.</param>
        /// <param name="filePath">The calendar file to be edited.</param>
        public static void EditAppointment(Appointment appt, string filePath)
        {
            Writer writer = new Writer(filePath);
            writer.EditAppointment(appt);
            writer.Close();
        }

        #endregion

        #region Private Methods

        private void Write(Appointment appt)
        {
            foreach (Appointment subAppt in appt.Occurrences)
            {
                Write(subAppt);
            }

            WriteLine(CalendarConstants.BEGIN_APPOINTMENT);

            foreach (Attendee attendee in appt.Attendees)
            {
                WriteLine(ConvertToString(attendee));
            }

            if (appt.Notes != null)
            {
                WriteLine(CalendarConstants.DESCRIPTION + ":" + ParserHelper.InsertEscapeChars(appt.Notes));
            }

            WriteLine(CalendarConstants.TIMESTAMP + ":" + DateParser.ToUTCString(appt.Timestamp));

            WriteLine(CalendarConstants.START_DATE_TIME + ConvertToString(appt.StartDateTime, appt.IsAllDayEvent));
            WriteLine(CalendarConstants.END_DATE_TIME + ConvertToString(appt.EndDateTime, appt.IsAllDayEvent));

            if (!string.IsNullOrEmpty(appt.Location))
            {
                WriteLine(CalendarConstants.LOCATION + ":" + ParserHelper.InsertEscapeChars(appt.Location));
            }

            if (appt.RecurrenceRule != null)
            {
                WriteLine(ConvertToString(appt.RecurrenceRule));
            }

            if (appt.IsPartOfSeries && appt.SequenceNumber != 0)
            {
                WriteLine(CalendarConstants.OCCURRENCE + ":" + ConvertToString(appt.Occurrence));
            }

            if (appt.IsPartOfSeries)
            {
                WriteLine(CalendarConstants.SEQUENCE_NUMBER + ":" + appt.SequenceNumber.ToString());
            }

            WriteLine(CalendarConstants.SUMMARY + ":" + ParserHelper.InsertEscapeChars(appt.Summary));
            WriteLine(CalendarConstants.UID + ":" + appt.UID.ToString().ToUpper());

            if (!string.IsNullOrEmpty(appt.URL))
            {
                WriteLine(CalendarConstants.URL + ":\"" + appt.URL + "\"");
            }

            if (appt.Reminder != null)
            {
                WriteLines(ConvertToString(appt.Reminder, appt.StartDateTime));
            }

            foreach (string line in appt.ExtraProperties)
            {
                WriteLine(line);
            }

            WriteLine(CalendarConstants.END_APPOINTMENT);
        }

        private void WriteLine(string line)
        {
            List<string> extraLines = new List<string>();

            if (line.Length <= CalendarConstants.LINE_LENGTH_LIMIT)
            {
                _streamWriter.WriteLine(line);
                return;
            }

            _streamWriter.WriteLine(line.Substring(0, CalendarConstants.LINE_LENGTH_LIMIT));
            WriteLine(' ' + line.Substring(CalendarConstants.LINE_LENGTH_LIMIT));
        }

        private void WriteLines(string lines)
        {
            foreach (string line in lines.Split(new string[] {_streamWriter.NewLine}, StringSplitOptions.None))
            {
                WriteLine(line);
            }
        }

        private void WriteCalendarEnd()
        {
            _streamWriter.WriteLine(CalendarConstants.END_CALENDAR);
            _streamWriter.Flush();
            _streamWriter.BaseStream.Seek(-1 * (CalendarConstants.END_CALENDAR.Length + _streamWriter.NewLine.Length), SeekOrigin.Current);
        }

        private string ConvertToString(DateTime dateTime)
        {
            return DateParser.ToUTCString(dateTime);
        }

        private string ConvertToString(DateTime dateTime, bool allDay)
        {
            if (allDay)
            {
                return ";" + CalendarConstants.DATE_VALUE + ":" + DateParser.ToDateString(dateTime);
            }

            return ":" + ConvertToString(dateTime);
        }

        private string ConvertToString(Reminder reminder, DateTime startDateTime)
        {
            string result = CalendarConstants.BEGIN_REMINDER + _streamWriter.NewLine;
            result = result + CalendarConstants.TRIGGER + ConvertTriggerToString(reminder, startDateTime) + _streamWriter.NewLine;
            result = result + CalendarConstants.REMINDER_UID + ":" + reminder.UID.ToString().ToUpper() + _streamWriter.NewLine;

            foreach (string line in reminder.ExtraProperties)
            {
                result = result + line + _streamWriter.NewLine;
            }

            result = result + CalendarConstants.END_REMINDER;
            return result;
        }

        private string ConvertToString(Attendee attendee)
        {
            string nameString = attendee.Name;

            if (attendee.Name.IndexOfAny(";:,\x8\xA1F\x7F\"".ToCharArray()) != -1)
            {
                nameString = "\"" + attendee.Name + "\"";
            }

            return CalendarConstants.ATTENDEE +
                (string.IsNullOrEmpty(attendee.Name) ? "" : ";" + CalendarConstants.COMMON_NAME + "=" + nameString) +
                ":" + CalendarConstants.MAILTO + ":" + attendee.EmailAddress;
        }

        private string ConvertToString(RecurrenceRule rule)
        {
            string result = CalendarConstants.RECURRENCE_RULE + ":" + CalendarConstants.FREQUENCY +
                "=" + ConvertToString(rule.Frequency);

            if (!rule.EndDateTime.Equals(new DateTime()))
            {
                result = result + ";" + CalendarConstants.RECURRENCE_END_DATE_TIME + "=" + DateParser.ToUTCString(rule.EndDateTime);
            }
            else if (rule.Count != 0)
            {
                result = result + ";" + CalendarConstants.COUNT + "=" + rule.Count.ToString();
            }

            if (rule.Interval != 0)
            {
                result = result + ";" + CalendarConstants.INTERVAL + "=" + rule.Interval.ToString();
            }

            if (rule.MonthWeekdays.Count > 0)
            {
                result = result + ";" + CalendarConstants.MONTH_WEEKDAY + "=" + ConvertToString(rule.MonthWeekdays);
            }
            else if (rule.MonthDate != 0)
            {
                result = result + ";" + CalendarConstants.MONTH_DATE + "=" + rule.MonthDate.ToString();
            }

            foreach (string property in rule.ExtraProperties)
            {
                result = result + ";" + property;
            }

            return result;
        }

        private string ConvertToString(Frequency freq)
        {
            return freq.ToString().ToUpper();
        }

        private string ConvertToString(IEnumerable<MonthWeekday> monthWeekdays)
        {
            List<string> weekdayStrings = new List<string>();

            foreach (MonthWeekday monthWeekday in monthWeekdays)
            {
                weekdayStrings.Add(
                    ((monthWeekday.WeekNumber == 0) ? "" : monthWeekday.WeekNumber.ToString()) +
                    ConvertToString(monthWeekday.Weekday));
            }

            return string.Join(",", weekdayStrings.ToArray());
        }

        private string ConvertToString(Weekday weekday)
        {
            return weekday.ToString().Substring(0, 2).ToUpper();
        }

        private string ConvertTriggerToString(Reminder reminder, DateTime startDateTime)
        {
            if (reminder.IsAbsoluteTrigger)
            {
                return ";" + CalendarConstants.DATE_TIME_VALUE + ":" + DateParser.ToUTCString(reminder.OnDateTrigger);
            }

            TimeSpan outputTimeSpan = reminder.Trigger.Negate();

            if (outputTimeSpan.Days >= 7)
            {
                return CalendarConstants.TRIGGER_PREFIX + (outputTimeSpan.Days / 7).ToString() + 'W';
            }
            else if (outputTimeSpan.Days > 0)
            {
                return CalendarConstants.TRIGGER_PREFIX + outputTimeSpan.Days.ToString() + 'D';
            }
            else if (outputTimeSpan.Hours > 0)
            {
                return CalendarConstants.TRIGGER_PREFIX + 'T' + outputTimeSpan.Hours.ToString() + 'H';
            }
            else if (outputTimeSpan.Minutes > 0)
            {
                return CalendarConstants.TRIGGER_PREFIX + 'T' + outputTimeSpan.Minutes.ToString() + 'M';
            }
            else if (outputTimeSpan.Seconds > 0)
            {
                return CalendarConstants.TRIGGER_PREFIX + 'T' + outputTimeSpan.Seconds.ToString() + 'S';
            }
            else
            {
                return CalendarConstants.ZERO_LENGTH_TRIGGER;
            }
        }

        private void Validate(IEnumerable<Appointment> appts)
        {
            foreach (Appointment appt in appts)
            {
                Validate(appt);
            }
        }

        private void Validate(Appointment appt)
        {
            if (!appt.Validate())
            {
                throw new CalendarException("Invalid appointment. All appointments must have a start time, end time, and summary.");
            }
        }

        #endregion
    }
}